feat: Integrate interactive Python shell into pyRevit with UI enhancements#3480
feat: Integrate interactive Python shell into pyRevit with UI enhancements#3480romangolev wants to merge 17 commits into
Conversation
- Create pyRevitLabs.PyRevit.Shell, an AvalonEdit-based WPF interactive Python console - Integrate shell compilation into dev scripts, labs build system, and command line utility - Add a new 'Python Shell' pulldown button under the pyRevit panel with Modal and Modeless shell launchers
Host the IronPython console as a pyRevit dockable pane so it stays open alongside the model. The pyRevitLabs.PyRevit.Shell assembly now exposes CreateDockableConsole, which builds a fully-wired IronPythonConsoleControl configured with the full pyRevit environment and marshals each statement through an ExternalEvent so Revit stays interactive while the pane is open. pyRevitCore registers the panel at startup with the same lifecycle as other pyRevit dockable panes.
Remove prebuilt pyRevitLabs.PyRevit.Shell.dll and ICSharpCode.AvalonEdit.dll from every engine folder under bin/. These are generated product binaries; the same pattern was used previously for pyRevitLabs.CommonWPF.dll.
Add BuildShellModule that builds pyRevitLabs.PyRevit.Shell for both IPY2712PR and IPY342 after BuildRuntimeModule. The csproj's DeployShell target copies pyRevitLabs.PyRevit.Shell.dll and ICSharpCode.AvalonEdit.dll into each engine folder, replacing the prebuilt binaries removed previously.
Introduce RevitThemeDetector (reflection-based so the same assembly compiles against pre-2024 Revits where UIThemeManager does not exist) and apply the detected theme to the console control and window/pane background across all three shell entry points: ShowModal, ShowModeless, and the dockable console. The dockable panel applies the same theme to its own background so the shell blends with the surrounding Revit chrome.
Update both the light and dark variants of the Python Shell pulldown icons.
Replace the Python Shell pulldown (Modal / Modeless / Dockable sub-buttons) with a single pushbutton. Shift+Click opens a small picker that persists the chosen mode in the script config; the next launch opens the shell as Modal, Modeless, or as the dockable pane accordingly. Icons move with the button. Rework the dockable pane registration so it fits the opt-in flow: - Pane now defaults to hidden; on the next Idling tick after startup the pane is re-hidden, since Revit persists DockablePane visibility across sessions and would otherwise reopen a pane the user never asked for. - The console is no longer built during Loaded. An Idling handler subscribed at registration time builds it once the pane has actually been shown, so the pyRevitLabs.PyRevit.Shell assembly is loaded on demand rather than at startup. - HOST_APP.uiapp is captured at startup (while the __revit__ builtin is still resolvable) and reused for late console creation. - Build failures now render the traceback inside the pane instead of leaving an empty surface with only a log entry.
Completion windows are created on demand by AvalonEdit, so they were always rendered with the default WPF light styling - jarring against a dark-themed console. Plumb the current theme from PythonConsoleControl through PythonConsolePad to PythonTextEditor, and have each freshly-created PythonConsoleCompletionWindow recolor its list, tooltip, and selection brushes when the dark theme is active. Light theme falls through to the default brushes.
…dows Add an EditorView (AvalonEdit text editor with Python syntax highlighting, file save and load, and F5 to run) paired with the existing IronPython console. Run sends the current selection, or the whole document, to the REPL through the same engine and dispatch path as the console-only shell. Two new shell entry points drive it: Shell.ModalEditor (Revit blocked, transaction can span several statements) and Shell.ModelessEditor (Revit stays interactive, dispatch via the existing ExternalEvent). ShellLauncher becomes generic over the window type via a new internal IShellWindow interface (ConsoleControl, ApplyTheme, SetRevitAsWindowOwner), so both the console-only and editor windows share the modal and modeless plumbing without duplication. InteractiveShellWindow now implements IShellWindow. The Python Shell button picker grows two options (Modal Editor, Modeless Editor) and the launcher script dispatches to the new entry points.
CreateDockableEditor was unusable: ConfigureEngineViaRuntime reaches InjectBuiltins, which reads the Ribbon via ComponentManager, and the Ribbon is owned by Revit's main UI thread - calling it from the REPL dispatcher raised InvalidOperationException. Marshal engine setup onto the main thread captured at launch (already the pattern for the existing shell entry points) and unwind TargetInvocationException via DescribeSetupError so the real failure (type, message, stack) shows in the REPL instead of a generic wrapper. Extract ConfigureControl so dockable and window-based shells share dispatch wiring, and add EnsureInteractiveBuiltins so reserved pyrevit builtins (e.g. __shiftclick__, __window__) get safe defaults if setup runs off-thread or fails. pyrevit.revit.events had a related bug: UI.ExternalEvent.Create needs a standard API context, but the module used to call it at import time. Importing it from the interactive shell (REPL runs statements on a non-command thread) crashed before the REPL could even start. Wrap the two top-level Create calls in try/except and replace them with lazy _get_unregisterer_extevent / _get_execute_extevent helpers that retry on first use. Wire the new variant into the UI: the Python Shell pushbutton picker grows a Docked Editor option, the launcher script recognises it, and pyRevitCore.startup now reads the same mode setting to pick CreateDockableEditor (or CreateDockableConsole) when the pane is first shown.
Mirrors ShellLauncher's modal path but drops the Revit-specific pieces (UIApplication, ExternalEvent, runtime-builtin injection) so the REPL and editor can be iterated on without starting Revit. The host references the already-deployed shell DLL from bin/netcore/engines/IPY2712PR/, is not referenced by any shipped solution or the build pipeline, and is therefore never built in CI or shipped to end users. Also wires a local-only appsettings.Development.json override into the build (gitignored) for engine/runtime tweaks during dev.
Replace the text-only toolbar items with Segoe Fluent Icons glyphs (with Win10 MDL2 fallback) plus labels, and add light/dark theme brush resources that are swapped at runtime by ApplyTheme alongside the editor and console palettes. Keeps the toolbar in sync with the rest of the shell's theme without bitmap assets.
Extract the toolbar brushes, control styles, and scrollbar templates into a shared ShellTheme resource dictionary plus a static ShellTheme.Apply helper that swaps the light/dark palette on any root. Both shell windows now merge ShellTheme.xaml, so a single Apply call keeps the editor, console, toolbar, scrollbars, and title bar in sync with the active Revit UI theme. Also drop the default chrome on InteractiveShellWindow / InteractiveEditorWindow and host a new ShellTitleBar UserControl (pyRevit icon, draggable, double-click to maximize, themed min/max/close buttons) so the borderless windows follow the shell palette instead of the OS caption. Stock overflow on the toolbar is collapsed on Loaded since the shell never overflows.
There was a problem hiding this comment.
PR Summary:
- Adds a full RevitPythonShell-style interactive Python shell (
pyRevitLabs.PyRevit.Shell) with an AvalonEdit-based REPL/editor, history, completion, syntax highlighting and theme matching. - Ships a new "Python Shell" pushbutton on the pyRevit panel with 6 open modes (Modal/Modeless/Docked × console/editor), plus a config.py to pick the mode.
- Wires statement execution through ExternalEvent so Revit stays interactive; hardens
pyrevit/revit/events.pyto tolerateExternalEvent.Createbeing called outside a valid API context (e.g. from the shell's REPL thread). - Adds build pipeline support (
BuildShellModule.cs) to deploy the shell DLL alongside each IronPython engine, and a dev-only host for iterating outside Revit.
Review Summary:
Reviewed the C# shell assembly, the events.py hardening, and the new/edited Python startup/pushbutton scripts against this repo's guidelines (IronPython 2.7 target compatibility, black/PEP8 formatting, and general code-review checklist patterns from prior PRs — e.g. silent-failure logging in initialization paths, ExternalEvent/queue handler design, vendored-dependency licensing conventions).
The most important finding: several new/modified Python files (events.py, startup.py, and the new pushbutton script.py/config.py) were saved with a leading UTF-8 BOM, which IronPython 2.7 does not strip — this can throw a SyntaxError at import/execution time and should be fixed before merge. I also flagged a concurrency hazard in ShellLauncher.cs where a non-thread-safe Queue<Action> and a single shared AutoResetEvent are used by multiple independently-running threads (REPL execution, paste/editor "Run", and background autocomplete), a silently-dropped scheduled callback in events.py when ExternalEvent creation fails, a missing blank line between two method defs (black/PEP8), and a missing license file for the vendored console-control code (consistent with this repo's licenses/ convention for every other vendored dependency).
Overall the shell architecture itself (lazy dockable-pane build via Idling, ExternalEvent-based dispatch, theme detection) is well designed; the issues above are concrete and actionable rather than architectural concerns.
Suggestions
- Re-save events.py, startup.py, script.py, and config.py as BOM-less UTF-8. Apply
- Add a licenses/ file attributing the vendored PythonConsoleControl (Joe Moorhouse) code. Apply
⚡ Apply all quick fixes (5 quick fixes)
| @@ -1,4 +1,4 @@ | |||
| """Revit events handler management.""" | |||
| """Revit events handler management.""" | |||
There was a problem hiding this comment.
A UTF-8 BOM (\ufeff) was accidentally introduced at the very start of this file (visible as the invisible character before """Revit events handler management."""). IronPython 2.7 does not strip a leading BOM when parsing .py source (unlike CPython 3), so a module imported this way can raise SyntaxError: invalid syntax on the first line. Since all pyRevit runtime code targets IronPython 2.7 per repo convention, this needs to be removed.
Please re-save this file as plain UTF-8 without signature (no BOM). The same issue appears in extensions/pyRevitCore.extension/startup.py, .../Python Shell.pushbutton/script.py and .../Python Shell.pushbutton/config.py — please check and fix those too.
actions
Quick Fix: Apply a small focused fix for this specific issue (takes < 1min).
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| @@ -1,8 +1,12 @@ | |||
| """pyRevit core startup script""" | |||
| """pyRevit core startup script""" | |||
There was a problem hiding this comment.
Same leading UTF-8 BOM issue as pyrevitlib/pyrevit/revit/events.py — this file starts with an invisible \ufeff before the docstring. This is a core startup script executed on every pyRevit session load; a BOM here can break IronPython 2.7 parsing (SyntaxError on import) since IronPython 2.7 doesn't auto-strip BOMs like CPython 3 does. Please re-save without the BOM.
actions
Quick Fix: Apply a small focused fix for this specific issue (takes < 1min).
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| @@ -0,0 +1,53 @@ | |||
| # -*- coding: utf-8 -*- | |||
There was a problem hiding this comment.
This new file also starts with a UTF-8 BOM (\ufeff) before the # -*- coding: utf-8 -*- comment. Same concern as noted on events.py/startup.py: IronPython 2.7 doesn't strip BOMs, so this can crash with a syntax error when the button is clicked. config.py in this same bundle has the identical issue. Please re-save both as BOM-less UTF-8.
actions
Quick Fix: Apply a small focused fix for this specific issue (takes < 1min).
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| public class ShellExternalEventDispatcher : IExternalEventHandler { | ||
| readonly IronPythonConsoleControl _consoleControl; | ||
| readonly Queue<Action> _commands = new Queue<Action>(); | ||
| readonly AutoResetEvent _commandCompleted; | ||
|
|
||
| public ShellExternalEventDispatcher(IronPythonConsoleControl consoleControl, AutoResetEvent commandCompleted) { | ||
| _consoleControl = consoleControl; | ||
| _commandCompleted = commandCompleted; | ||
| } | ||
|
|
||
| public void Enqueue(Action command) { | ||
| _commands.Enqueue(command); | ||
| } | ||
|
|
||
| public void Execute(UIApplication app) { |
There was a problem hiding this comment.
ShellExternalEventDispatcher uses a plain (non-thread-safe) Queue<Action> plus a single shared AutoResetEvent (commandCompleted), and the same dispatch closure/handler instance is wired (in ConfigureControl/AttachAndDispatch, lines ~115-121) to both host.Console.SetCommandDispatcher(dispatch) and host.Editor.SetCompletionDispatcher(dispatch).
SetCommandDispatcher installs dispatch as IronPython's own command dispatcher (PythonContext.GetSetCommandDispatcher), which is invoked from the console host's background REPL read-eval thread for typed statements, and also from PythonConsole.ExecuteStatements() (on PythonConsole's dedicated dispatcherThread) for pasted/multi-line and editor "Run" statements. SetCompletionDispatcher is invoked from PythonTextEditor's independent background completion thread (PythonTextEditor.Completion() → BackgroundShowCompletionWindow/BackgroundUpdateCompletionDescription).
So up to three independently-running threads can call the same dispatch concurrently (e.g. autocomplete firing while a pasted block or editor run is still executing). Two threads calling handler.Enqueue(...) at the same time on the same Queue<Action> is unsafe (Queue<T> is not thread-safe for concurrent writers) and can corrupt internal state or throw. Separately, because commandCompleted is shared, AutoResetEvent.Set() from Execute() can release any currently-waiting caller, not necessarily the one whose command actually finished — so a completion-dispatch caller could return from WaitOne() before its own action has run (or vice-versa), racing with the actual work.
Suggest either (a) a thread-safe collection (ConcurrentQueue<Action>) with a per-call synchronization primitive (queue (Action, ManualResetEventSlim) tuples and wait on the item's own handle), or (b) giving completion its own dedicated ExternalEvent/handler pair so it can't interleave with statement execution on the shared queue.
actions
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| _HANDLER.schedule(_wrapper) | ||
| _EXTERNAL_EVENT.Raise() | ||
| _ext_event = _get_execute_extevent() | ||
| if _ext_event is not None: | ||
| _ext_event.Raise() |
There was a problem hiding this comment.
If _get_execute_extevent() returns None (i.e. ExternalEvent.Create failed both at import time and on this lazy retry), _wrapper has already been pushed onto _HANDLER._queue on the previous line, but Raise() is never called. The queued closure — and whatever func/args/kwargs it closed over — just sits in the queue forever with no execution and no error surfaced to the caller. execute_in_revit_context() silently returns as if it scheduled work.
Per repo convention ("silent failures in initialization/critical paths must be logged"), this should at minimum log an error so callers can diagnose why their scheduled callback never ran, e.g.:
_ext_event = _get_execute_extevent()
if _ext_event is not None:
_ext_event.Raise()
else:
mlogger.error("Could not create ExternalEvent; scheduled callback will not run")Same applies to delayed_unregister_exec_handlers above.
actions
Quick Fix: Apply a small focused fix for this specific issue (takes < 1min).
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| @@ -0,0 +1,150 @@ | |||
| // Copyright (c) 2010 Joe Moorhouse | |||
There was a problem hiding this comment.
This file (and most of Console/*.cs in this new project) carries a // Copyright (c) 2010 Joe Moorhouse header, indicating it's vendored/ported from a third-party project (the IronPython console control originally from SharpDevelop/RevitPythonShell). Every other vendored dependency in this repo ships an accompanying license file under licenses/ (e.g. PYTHONNET_LICENSE.txt, MAHAPPS.METRO_LICENSE.txt). Please add the upstream license (and ideally a pointer to the source repo) for this ported console control, and likewise for the bundled ICSharpCode.AvalonEdit dependency if not already covered, to keep attribution consistent with repo convention.
actions
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
| ) | ||
| def _attach_console(self): |
There was a problem hiding this comment.
Missing blank line between _apply_panel_theme and _attach_console method definitions — def _attach_console(self): immediately follows the closing ) of the previous method with no separating blank line. Repo convention (black/PEP8) requires a blank line between method definitions inside a class body. Please add a blank line before def _attach_console(self):.
actions
Quick Fix: Apply a small focused fix for this specific issue (takes < 1min).
Feedback: Rate this comment to help me improve future code reviews:
- 👍 Good - Helpful and accurate
- 👎 Poor - Wrong, unclear, or unhelpful
- Skip if you don't have any strong opinions either way.
There was a problem hiding this comment.
Pull request overview
This PR introduces a RevitPythonShell-style interactive Python REPL (with optional AvalonEdit editor) integrated directly into pyRevit, including a new shell assembly, a ribbon pushbutton to launch it in multiple modes (modal/modeless/docked), and supporting runtime/build pipeline updates so commands execute in a valid Revit API context.
Changes:
- Adds a new
pyRevitLabs.PyRevit.ShellWPF-based interactive shell (console + optional editor) with theme matching and ExternalEvent-based dispatch for modeless/docked scenarios. - Registers a new dockable panel at startup and adds a new “Python Shell” pushbutton with a Shift+Click configuration UI.
- Extends dev/build tooling to build and deploy the shell per IronPython fork and to expose runtime engine configuration for interactive usage.
Reviewed changes
Copilot reviewed 50 out of 54 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pyrevitlib/pyrevit/revit/events.py | Makes ExternalEvent creation tolerant to non-API import contexts and defers creation/raising logic. |
| extensions/pyRevitCore.extension/startup.py | Registers a dockable “pyRevit Python Shell” panel and lazily builds the hosted shell control on first show. |
| extensions/pyRevitCore.extension/PythonShellDockablePanel.xaml | Adds a minimal dockable pane host surface for the shell control. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Python Shell.pushbutton/script.py | Adds pushbutton script to open the shell in modal/modeless/docked (+ editor) modes. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Python Shell.pushbutton/config.py | Adds Shift+Click configuration UI for selecting shell open mode. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Python Shell.pushbutton/bundle.yaml | Defines UI metadata (title/tooltip/context) for the new pushbutton. |
| extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/bundle.yaml | Adds “Python Shell” into the pyRevit panel layout. |
| dev/scripts/configs.py | Adds a dev path constant for the shell project. |
| dev/pyRevitLabs.PyRevit.Shell/Themes/ShellTheme.xaml | Introduces shared theme brushes and control styles for shell UI chrome/scrollbars. |
| dev/pyRevitLabs.PyRevit.Shell/Themes/ShellTheme.cs | Adds runtime theme brush swapping for light/dark palettes. |
| dev/pyRevitLabs.PyRevit.Shell/ShellTitleBar.xaml.cs | Implements custom draggable title bar behavior for borderless shell windows. |
| dev/pyRevitLabs.PyRevit.Shell/ShellTitleBar.xaml | Defines the title bar UI with minimize/maximize/close buttons and icon. |
| dev/pyRevitLabs.PyRevit.Shell/ShellLauncher.cs | Implements shell window/control creation and Revit-context dispatching via ExternalEvent. |
| dev/pyRevitLabs.PyRevit.Shell/Shell.cs | Exposes public entry points for launching shell windows and creating dockable controls. |
| dev/pyRevitLabs.PyRevit.Shell/RevitThemeDetector.cs | Detects Revit’s current UI theme via reflection for compatibility across versions. |
| dev/pyRevitLabs.PyRevit.Shell/pyRevitLabs.PyRevit.Shell.csproj | Adds the new shell project targeting net48 and net8.0-windows with embedded resources. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveShellWindow.xaml.cs | Hosts the REPL-only shell window and applies theme/owner behavior. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveShellWindow.xaml | Defines the shell window (borderless) layout for the console-only variant. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveEditorWindow.xaml.cs | Hosts the editor+REPL window and applies theme/owner behavior. |
| dev/pyRevitLabs.PyRevit.Shell/InteractiveEditorWindow.xaml | Defines the shell window layout for the editor+console variant. |
| dev/pyRevitLabs.PyRevit.Shell/EditorView.xaml.cs | Implements AvalonEdit editor actions (open/save/run) and theme synchronization with console. |
| dev/pyRevitLabs.PyRevit.Shell/EditorView.xaml | Defines editor+console UI (toolbar, editor, splitter, console). |
| dev/pyRevitLabs.PyRevit.Shell/Directory.Build.targets | Adds per-IronPython-fork build configurations and deploys shell + AvalonEdit into engine folders. |
| dev/pyRevitLabs.PyRevit.Shell/Console/Resources/Python.xshd | Adds a light syntax-highlighting definition for Python. |
| dev/pyRevitLabs.PyRevit.Shell/Console/Resources/Python-Dark.xshd | Adds a dark syntax-highlighting definition for Python. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonTextEditor.cs | Adapts/extends console editor behaviors including themed completion popups. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonOutputStream.cs | Writes engine output into the console editor and decodes unicode escapes. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonEditingCommandHandler.cs | Implements editing command behaviors (cut/copy/delete) for the console editor. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsolePad.cs | Wraps console host/control and applies theme settings (including completion theme). |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleHost.cs | Hosts the IronPython console runtime and wires output/engine initialization. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleHighlightingColorizer.cs | Adds a custom colorizer that distinguishes input vs output lines. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleControl.xaml.cs | Implements WPF control hosting and theme/highlighting application logic. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleControl.xaml | Defines the WPF wrapper control for the embedded console editor. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleCompletionWindow.cs | Implements completion UI and applies dark theme styling to list/tooltip. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsoleCompletionDataProvider.cs | Provides completion items/descriptions, including fork-agnostic iteration behavior. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonConsole.cs | Implements the interactive console behaviors, history, dispatching, and statement execution. |
| dev/pyRevitLabs.PyRevit.Shell/Console/PythonCompletionData.cs | Defines AvalonEdit completion item entries. |
| dev/pyRevitLabs.PyRevit.Shell/Console/CommandLineHistory.cs | Implements command history navigation for the console. |
| dev/pyRevitLabs.PyRevit.Shell.DevHost/README.md | Documents the dev-only host app for iterating on the shell outside Revit. |
| dev/pyRevitLabs.PyRevit.Shell.DevHost/pyRevitShellDevHost.csproj | Adds a dev-only WinExe project referencing deployed engine DLLs with optional pre-build shell build. |
| dev/pyRevitLabs.PyRevit.Shell.DevHost/Program.cs | Adds a standalone host executable that launches the shell UI outside Revit for UI iteration. |
| dev/pyRevitLabs.PyRevit.Runtime/IronPythonEngine.cs | Refactors builtins injection into a reusable method for interactive shell configuration. |
| dev/pyRevitLabs.PyRevit.Runtime/InteractiveEngine.cs | Adds a runtime entry point for configuring an interactive engine like a normal pyRevit run. |
| dev/pyrevit.py | Adds a pyrevit build shell CLI target. |
| dev/_labs.py | Implements the build_shell CLI command to build per-fork configurations. |
| dev/_build.py | Ensures shell build runs as part of the dev “build binaries” sequence. |
| build/Program.cs | Registers the new BuildShellModule into the build pipeline. |
| build/Modules/BuildShellModule.cs | Adds a build module to build/deploy the shell after runtime builds. |
| build/Helpers/PyRevitPaths.cs | Adds a path helper for the shell project. |
| build/Build.csproj | Copies Development appsettings to output (for local overrides). |
| .gitignore | Ignores local-only build/appsettings.Development.json. |
| _HANDLER.schedule(_wrapper) | ||
| _EXTERNAL_EVENT.Raise() | ||
| _ext_event = _get_execute_extevent() | ||
| if _ext_event is not None: | ||
| _ext_event.Raise() |
| def delayed_unregister_exec_handlers(handler_group_id): | ||
| HANDLER_UNREGISTERER.handler_group_id = handler_group_id | ||
| HANDLER_UNREGISTERER_EXTEVENT.Raise() | ||
| ext_event = _get_unregisterer_extevent() | ||
| if ext_event is not None: | ||
| ext_event.Raise() |
| def _load_shell(): | ||
| # The shell DLL ships into the active engine folder (the IronPython fork pyRevit runs), | ||
| # alongside the loaded pyRevitLoader; loading it from there makes the shell use that engine. | ||
| engine_dir = None | ||
| for asm in AppDomain.CurrentDomain.GetAssemblies(): | ||
| if asm.GetName().Name == "pyRevitLoader": | ||
| engine_dir = Path.GetDirectoryName(asm.Location) | ||
| break | ||
| clr.AddReferenceToFileAndPath( | ||
| Path.Combine(engine_dir, "pyRevitLabs.PyRevit.Shell.dll") | ||
| ) | ||
| from PyRevitLabs.PyRevit.Shell import Shell | ||
|
|
||
| # Forward this engine's sys.path so `from pyrevit import ...` resolves in the shell as here. | ||
| search_paths = List[str]() | ||
| for path in sys.path: | ||
| search_paths.Add(path) | ||
| return Shell, search_paths |
| public void Enqueue(Action command) { | ||
| _commands.Enqueue(command); | ||
| } | ||
|
|
||
| public void Execute(UIApplication app) { | ||
| while (_commands.Count > 0) { | ||
| var command = _commands.Dequeue(); | ||
| try { | ||
| command(); | ||
| } | ||
| catch (Exception ex) { | ||
| _consoleControl.WithConsoleHost(host => { | ||
| var formatter = host.Engine.GetService<ExceptionOperations>(); | ||
| host.Console.WriteLine(formatter.FormatException(ex), Style.Error); | ||
| }); | ||
| } | ||
| finally { | ||
| _commandCompleted.Set(); | ||
| } | ||
| } | ||
| } |
| // Poll the dispatcher operation so a Ctrl+C keyboard interrupt can break a long run. | ||
| static void RunOnDispatcher(Dispatcher dispatcher, Action command) { | ||
| if (command == null) | ||
| return; | ||
| var operation = dispatcher.BeginInvoke(DispatcherPriority.Normal, command); | ||
| while (operation.Status != DispatcherOperationStatus.Completed) | ||
| operation.Wait(TimeSpan.FromSeconds(1)); | ||
| } |
| if (!File.Exists(Path.Combine(engineDir, "pyRevitLabs.PyRevit.Shell.dll"))) | ||
| { | ||
| Console.Error.WriteLine( | ||
| "The shell is not built yet. Build it first, e.g.:" + Environment.NewLine + | ||
| " cd build && dotnet run -c Release -- ci" + Environment.NewLine + | ||
| "or just the shell:" + Environment.NewLine + | ||
| " dotnet build dev/pyRevitLabs.PyRevit.Shell/pyRevitLabs.PyRevit.Shell.csproj -c \"Release IPY2712PR\""); | ||
| return 1; | ||
| } |
Description
Adds a RevitPythonShell-style interactive Python shell directly into pyRevit, so users can run Python statements against the live Revit API without leaving Revit. Ships as a new pyRevitLabs.PyRevit.Shell assembly plus a Python Shell pushbutton on the pyRevit panel.
Key pieces:
Dockable variants register through f the console builds lazily on firstshow via an Idling handler. Mode is configurable via Shift+Click on the pushbutton.
creates its module-level ExternalEveom the shell's REPL thread isn'talways a valid API context.
Additional Notes